跳至主要内容

課程:JavaScript 與 React 底層原理 第 8 堂:ES6+ 語法基礎

23 解構賦值

在我們深入探討非同步 JavaScript 的事件循環(Event Loop)與 Promise 之後,我們已經掌握了 JS 處理「時間」與「任務排程」的能力。現在,我們要將視角轉向 JS 處理「資料結構」的利器——ES6+ 現代語法工具包。

想像一下,你正在經營一家大型圖書館,每天有成千上萬本書進出。如果你每次要找一本書的作者,都必須先走進倉庫、打開箱子、翻到扉頁、讀取名字、最後再把箱子封好放回原處,這過程不僅繁瑣且充滿重複勞動。

在 ES6 之前,從陣列或物件中提取資料就像是這種傳統做法:

const user = { name: 'Aria', age: 25, role: 'Admin' };
const name = user.name;
const age = user.age;
const role = user.role;

這段程式碼充滿了重複的 user.解構賦值(Destructuring Assignment) 的出現,正是為了讓我們能用一種更優雅、更具「宣告式(Declarative)」的方式,直接描述我們想要的資料形狀,並從結構中將它們「提取」出來。

什麼是解構賦值?

解構賦值並不是一種全新的資料類型,而是一種語法糖。它的核心概念是模式匹配(Pattern Matching)

所謂模式匹配,是指 JS 引擎會觀察等號左邊的「模式」與等號右邊的「資料結構」是否吻合。如果吻合,它就會自動將對應位置的值指派給左邊定義的變數。這就像是你有一個專門用來裝特定形狀積木的模具,只要把整箱積木倒下去,符合形狀的積木就會自動掉進對應的格子裡。

在 React 的開發世界中,解構賦值幾乎無處不在:從 useState 的回傳值處理,到元件 Props 的拆解,它是讓 React 程式碼保持簡潔、可讀的靈魂技術。


陣列解構:位置決定一切

陣列解構的關鍵在於其順序性。因為陣列的本質是元素的有序集合,所以解構時,變數的命名並不重要,重要的是它們在左側中括號內的位置

基本語法與位置對應

const colors = ['#ff0000', '#00ff00', '#0000ff'];

// 傳統寫法
// const red = colors[0];
// const green = colors[1];

// 解構寫法
const [red, green, blue] = colors;

console.log(red); // "#ff0000"
console.log(green); // "#00ff00"

在這裡,red 拿到了第一個元素,green 拿到了第二個,這就是位置對應。

跳過不需要的元素

有時候我們只對陣列中的某幾個位置感興趣,而想忽略其他的。這時我們不需要為每個位置都命名變數,只需要留下「空白」即可:

const coordinates = [10, 20, 30, 40];

// 我們只想要第一與第三個值(X 與 Z 軸)
const [x, , z] = coordinates;

console.log(x); // 10
console.log(z); // 30

這種「留白」技巧在處理函數回傳多個值的陣列時非常實用。

連結 React:為什麼 useState 回傳的是陣列?

你可能已經很習慣寫 const [count, setCount] = useState(0)。你有想過為什麼 React 團隊要把 useState 設計成回傳陣列,而不是回傳物件嗎?

這正是利用了陣列解構**「命名自由、位置固定」**的特性:

  1. 命名彈性:因為是陣列解構,我們可以自由命名變數(如 counttodoListisLoaded)。
  2. 避免衝突:如果 useState 回傳物件 { state, setState },那麼在同一個元件中使用多次 Hook 時,我們就必須不斷地進行「重新命名」來避免變數衝突。
// 如果 useState 回傳物件,寫起來會很痛苦:
const { state: count, setState: setCount } = useState(0);
const { state: name, setState: setName } = useState('Aria');

// 陣列解構讓這一切變得極其自然:
const [count, setCount] = useState(0);
const [name, setName] = useState('Aria');

這種設計體現了對開發者體驗(DX)的極致追求。


物件解構:鍵名(Key)才是關鍵

與陣列不同,物件解構不看順序,而是看鍵名(Key Name)。只要左側變數名稱與右側物件的屬性名稱一致,就能成功匹配。

基本匹配與鍵名對應

const laptop = {
brand: 'Apple',
model: 'MacBook Pro',
year: 2023
};

const { brand, year } = laptop;

console.log(brand); // "Apple"
console.log(year); // 2023

注意到即使我們沒有按照 brand, model, year 的順序提取,JS 也能正確找到 year。這大大降低了記憶參數順序的負擔。

重新命名(Aliasing)

有時候,物件中的屬性名稱可能與我們當前的變數名稱衝突,或者我們單純覺得該名稱不夠語意化。這時我們可以使用 : 來重新命名:

const response = {
data: { id: 1, title: 'Learn React' },
status: 200
};

// 將 data 重新命名為 todo,將 status 重新命名為 httpCode
const { data: todo, status: httpCode } = response;

console.log(todo.title); // "Learn React"
console.log(httpCode); // 200

警告: 這是一個常見的語法混淆點。在解構中,{ 原始名稱: 新變數名稱 } 的冒號代表的是「指派給」,而不是物件字面量中的「鍵值對」。

預設值(Default Values)

當我們嘗試解構一個物件中不存在的屬性時,會得到 undefined。為了增加程式碼的魯棒性(Robustness),我們可以設定預設值:

const user = { name: 'Aria' };

const { name, role = 'Guest' } = user;

console.log(role); // "Guest" (因為 user 物件中沒有 role)

這在處理 API 回傳資料時非常重要,因為我們無法保證後端永遠會給出所有的欄位。

連結 React:元件 Props 的解構

在 React 元件中,Props 本質上是一個大物件。如果不使用解構,程式碼會長這樣:

const UserProfile = (props) => {
return (
<div>
<h1>{props.name}</h1>
<p>{props.bio}</p>
</div>
);
};

當 Props 變多時,滿畫面的 props. 會讓視線變得很雜亂。透過解構,我們可以直接在參數欄位進行拆解:

const UserProfile = ({ name, bio, role = 'Standard User' }) => {
return (
<div>
<h1>{name} ({role})</h1>
<p>{bio}</p>
</div>
);
};

這樣做不僅簡潔,還能一眼看出這個元件「依賴」了哪些資料。此外,這也方便我們直接在參數處設定預設值。


巢狀解構(Nested Destructuring)

有時候資料結構非常深層,我們想一次提取最內層的資料。解構語法支援層層嵌套的匹配模式。

深度提取範例

假設我們有一份複雜的訂單資料:

const order = {
id: 'ORD-123',
customer: {
info: {
firstName: 'Aria',
lastName: 'Chen'
},
email: 'aria@example.com'
}
};

// 一次提取到最深層
const {
customer: {
info: { firstName }
}
} = order;

console.log(firstName); // "Aria"

注意: 在上述語法中,customerinfo 並沒有被定義成變數,它們只是用來導航到目標位置的「路徑」。如果你也需要這兩個變數,則需要額外列出。

關於巢狀解構的警示

雖然巢狀解構看起來很強大,但過度使用會導致可讀性懸崖

  1. 可讀性變差:結構過深時,閱讀程式碼的人會很難一眼看出資料的層級關係。
  2. 執行風險:這是一個極大的陷阱——如果 order.customerundefined,那麼當你嘗試解構 customer: { info } 時,JS 會拋出 TypeError。解構賦值無法自動處理 **null****undefined** 的中間層級

專家建議: 如果結構超過兩層,建議分次解構,或者搭配我們下一堂課會學到的「選擇性鏈結(Optional Chaining, ?.)」來確保安全性。


解構賦值的邊界情況與陷阱

為了真正精通這門技術,我們必須了解它在什麼情況下會失效。

1. 不能解構 null 與 undefined

如果你嘗試對 nullundefined 進行解構,程式會直接崩潰。

const { a } = null; // ❌ TypeError: Cannot destructure property 'a' of 'null'

這就是為什麼在解構 API 資料前,最好先確保該資料已存在,或是給予整個解構目標一個空物件作為後盾(Fallback)。

2. 變數宣告的限制

如果你想對已經宣告過的變數進行物件解構,不能直接寫 { a } = obj;,因為 JS 會把大括號開頭的語句當成一個「程式區塊(Block)」。你必須用圓括號包起來:

let a, b;
({ a, b } = { a: 1, b: 2 }); // ✅ 正確做法

雖然這種情況在 React 開發中較少見(我們通常傾向於使用 const 建立新變數),但這是面試中常見的陷阱題。


為什麼這能讓程式碼更具宣告式(Declarative)風格?

在命令式(Imperative)風格中,我們告訴電腦「如何去做」:

  • 「去把 user 物件拿來」
  • 「找出裡面叫 name 的屬性」
  • 「把它存進一個叫 userName 的箱子裡」

而在解構賦值的宣告式風格中,我們告訴電腦「我要什麼」:

  • 「我要從這個物件中提取出符合 { name: userName } 模式的資料」

這種思維轉變與 React 的設計哲學不謀而合。React 讓我們宣告 UI 長什麼樣子,而解構讓我們宣告資料應該如何被映射到變數上。這減少了中間的「過程程式碼」,讓邏輯一目瞭然。

總結與銜接

解構賦值徹底改變了我們在 JS 中處理資料的方式。它不僅僅是減少了幾行程式碼,更重要的是它建立了一種結構化的對應關係,讓開發者能更專注於資料本身的內容,而非提取資料的過程。

本章核心回顧

  • 陣列解構:基於位置,適合用於回傳值命名不固定的情境(如 useState)。
  • 物件解構:基於鍵名,適合處理 Props 或 API 回傳的大型物件,具備重新命名與預設值功能。
  • 巢狀解構:可深度提取,但需注意 undefined 導致的報錯風險。
  • React 連結:它是 Hook、Props 以及所有狀態管理邏輯的基石。

接下來的學習目標

我們已經學會了如何從一個結構中精準地「取出」特定資料。然而,在 React 的開發中,我們經常需要處理「資料的合併與複製」,尤其是在更新狀態(State)時,必須遵守**不可變性(Immutability)**原則。

在下一部分,我們將探討 Spread(展開)與 Rest(其餘)運算子。這對雙胞胎語法將告訴你,如何輕鬆地複製一個物件並修改其中一個屬性,以及如何將剩餘的資料收集起來。這是理解 React 狀態更新邏輯最關鍵的一環。